iT邦幫忙

2024 iThome 鐵人賽

DAY 20
0
JavaScript

Signal API in Angular系列 第 20

Day 20 - viewChildren函數介紹

  • 分享至 

  • xImage
  •  

從 Angular 17 開始,團隊開始將查詢裝飾器 (query decorators) 遷移到 signal。此後以下查詢裝飾器已遷移。

@ViewChild -> viewchild()
@ViewChildren -> viewChildren()  
@contentChild -> contentChild()
@contentChildren -> contentChildren()

今天,我將介紹 viewChildren,它可以透過範本變數 (template variables)、 directives 或組件類型 (component type) 進行查詢。

ViewChildren 裝飾器和 viewChildren 函數之間的區別

  • ViewChildren 裝飾器傳回一個 QueryList,而 viewChildren 函數傳回一個 Signal<ReadT[]>
  • viewChildren 函數的第二個參數是具有 read 屬性的選項物件。然後,應用程式檢索相同類型的元素。

viewChildren 的生命週期

@for (i of [0, 1, 2, 3, 4, 5, 6, 7]; track $index) {
   <button #el (click)="updateVariableClicked(i)">{{ i + 1 }}</button>
}

els = viewChildren('el', { read: ElementRef<HTMLButtonElement[]> });

constructor() {
   console.log('refs', this.els().length);  // 0
} 

ngAfterViewInit(): void {
   console.log('ngAfterViewInit, refs ->', this.els().length); // 8
}

ngOnInit(): void {
   console.log('ngOnInit, refs ->', this.els().length); // 0
}

viewChildren 函數在 AfterViewInit 執行後可用。 在 constructorngOnInit 中,元素數量為0。 在 ngAfterViewInit 中,元素數量為 8。

在以下例子中,我將展示 viewChildren 如何透過範本變數 (template variables)、directives 和 Angular 組件進行查詢。

例子 1:透過範本變數 (template variables)查詢元素

import { Component, ElementRef, signal, viewChildren } from '@angular/core';
import { NgTemplateOutlet } from '@angular/common';

@Component({
 selector: 'app-query-by-variable',
 standalone: true,
 imports: [NgTemplateOutlet],
 template: `
   <div class="container">
     <div>
       <h3>Query by  the template variables</h3>
       <p>Select a template</p>
       @for (i of [0, 1, 2, 3, 4, 5, 6, 7]; track $index) {
         <button #el (click)="updateVariableClicked(i)">{{ i + 1 }}</button>
       }
     </div>
     <ng-container *ngTemplateOutlet="t; context: { $implicit: variableBtn() }" />
   </div>

   <ng-template #t let-id>
     <p>Simple Template {{ id }}</p>
   </ng-template>
 `,
})
export class AppQueryByVariableComponent {
 variableBtn = signal(1);

 els = viewChildren('el', { read: ElementRef<HTMLButtonElement[]> });

 updateVariableClicked(index: number) {
   const prevValue = this.variableBtn();
   this.variableBtn.set(index + 1);

   const styles = this.els().map((e) => e.nativeElement.style);
   styles[prevValue - 1].backgroundColor = 'rgb(239, 239, 239)'
   styles[index].backgroundColor = 'goldenrod';
 }
}
els = viewChildren('el', { read: ElementRef<HTMLButtonElement[]> });

viewChildren 函數透過範本變數 #el 查詢按鈕清單。選擇器是 el,第二個參數 { read: ElementRef<HTMLButtonElement[]> } 確保檢索 ElementRef 陣列。

updateVariableClicked(index: number) {
   const prevValue = this.variableBtn();
   this.variableBtn.set(index + 1);

   const styles = this.els().map((e) => e.nativeElement.style);
   styles[prevValue - 1].backgroundColor = 'rgb(239, 239, 239)';
   styles[prevValue - 1].borderStyle = 'solid';
   styles[prevValue - 1].borderWidth = '1px';
   styles[index].backgroundColor = 'goldenrod';
   styles[index].borderStyle = 'dashed';
   styles[index].borderWidth = '0.25rem';
 }

當使用者點擊按鈕時,將執行 updateVariableClicked 方法來更新 variableBtn signal。 ngtTemplate context 接收 variableBtn 值並在範本中顯示該數字。 此外,先前選擇的按鈕會刪除背景顏色,並且所選按鈕的背景顏色會更新為金黃色。

例子 2:透過 Directive 查詢元素

import { Directive, ElementRef, inject } from "@angular/core";

@Directive({
 selector: 'button[appSelectTemplate]',
 standalone: true,
})
export class AppSelectTemplateDirective {
 style = inject<ElementRef<HTMLButtonElement>>(ElementRef).nativeElement.style;

 changeBackgroundColor(color = 'rgb(239, 239, 239)') {
   const isRevert = color === 'rgb(239, 239, 239)';
   this.style.backgroundColor = color;
   this.style.borderWidth = isRevert ? '1px' : '0.25rem';
   this.style.borderStyle = isRevert ? 'solid' : 'dashed';
 }
}

AppSelectTemplateDirective directive提供了一種更新按鈕的背景顏色、邊框寬度和邊框樣式的方法。當選擇按鈕時,邊框寬度增加到 0.25rem,邊框樣式為虛線。當取消選擇按鈕時,邊框寬度和按鈕樣式分別恢復為 1px 和實心。

import { Component, signal, viewChildren } from '@angular/core';
import { NgTemplateOutlet } from '@angular/common';
import { AppSelectTemplateDirective } from './select-template.directive';

@Component({
 selector: 'app-query-by-directive',
 standalone: true,
 imports: [NgTemplateOutlet, AppSelectTemplateDirective],
 template: `
   <div class="container">
     <div>
       <h3>Query by  the type of directive</h3>
       <p>Click a button to select a template</p>
       @for (i of [0, 1, 2, 3]; track $index) {
         <button appSelectTemplate (click)="updateLastClicked(i)">{{ i + 1 }}</button>
       }
     </div>
     <ng-container *ngTemplateOutlet="t; context: { $implicit: lastClickedBtn() }" />
   </div>

   <ng-template #t let-id>
     <p>Simple Template {{ id }}</p>
   </ng-template>`,
})
export class AppQueryByDirectiveComponent {
 lastClickedBtn = signal(1);

 selectTemplateDirectives = viewChildren(AppSelectTemplateDirective);

 updateLastClicked(index: number) {
   const prevValue = this.lastClickedBtn();
   const directives = this.selectTemplateDirectives();
   directives[prevValue - 1].changeBackgroundColor();
   directives[index].changeBackgroundColor('cyan');
   this.lastClickedBtn.set(index + 1);
 }
}
<button appSelectTemplate (click)="updateLastClicked(i)">{{ i + 1 }}</button>

AppQueryByDirectiveComponent 組件指定按鈕的 appSelectLabel 屬性。

selectedDirectives = viewChildren(AppSelectTemplateDirective);

viewChildren 函數查詢 AppSelectTemplateDirective directives。當使用者按一下該按鈕時,將執行 updateLastClicked 方法。

updateLastClicked(index: number) {
   const prevValue = this.lastClickedBtn();
   const directives = this.selectedDirectives();
   directives[prevValue - 1].changeBackgroundColor();
   directives[index].changeBackgroundColor('cyan');
   this.lastClickedBtn.set(index + 1);
 }

此方法恢復前一個按鈕的樣式; 所選按鈕具有青色背景、0.25rem 邊框寬度和虛線。

例子 3:按類型查詢 Angular 元件

import { Component, ChangeDetectionStrategy, signal, computed } from '@angular/core';

const imgURL = 'https://picsum.photos/300/200';

@Component({
 selector: 'app-photo',
 standalone: true,
 template: `
   <div class="photo">
     <img [src]="img()" alt="Random picture" />
   </div>
 `,
})
export default class PhotoComponent {
 random = signal(Date.now());

 img = computed(() => `${imgURL}?random=${this.random()}`)
}

PhotoComponent 組件有一個 signal 來定義圖片 URL 的隨機種子。 當任何 signal value 更新時,img computed signal 會產生新的圖像 URL。

import { ChangeDetectionStrategy, Component, viewChildren } from '@angular/core';
import AppPhotoComponent from './query-component/photo.component';

@Component({
 selector: 'app-root',
 standalone: true,
 imports: [AppPhotoComponent],
 template: `
   <h3>Query by components</h3>
   <div class="photos">
     @let photos = photoComponents();
     <div class="card">
       <app-photo class="photo" />
       <button (click)="changeImage(photos[0])">Change photo 1</button>
     </div>
     <div class="card">
       <app-photo class="photo" />
       <button (click)="changeImage(photos[1])">Change photo 2</button>
     </div>
     <div>
       <app-photo class="photo" />
       <button (click)="changeImage(photos[2])">Change photo 3</button>
     </div>
   </div>
 `,
})
export class App {
 name = 'iTHome Ironman 2024 day 20';
 description = 'Introduction to viewChildren';

 photoComponents = viewChildren(AppPhotoComponent);

 changeImage(photo: AppPhotoComponent) {
   photo.random.set(Date.now());
 }
}

App 組件使用 viewChildren 函數查詢 PhotoComponentApp 組件存取 photoComponents 變數 以將 photo 遞給 changeImage 方法。當使用者按一下該按鈕時,該組件會顯示一個新圖像。

結論:

  • viewChildren 可以查詢 elements、directives 和 components。第一個參數是一個 selector,它是範本變數或類型。
  • read 屬性指定 viewChildren 傳回的元素類型。
  • viewChildren 函數傳回 signal, 值是一個 array 。當函數找不到匹配項時,它會傳回一個 empty array,而不是錯誤。
  • viewChildren 在 AfterViewInit hook 之後可用。

鐵人賽第 20 天就這樣結束了。

參考:


上一篇
Day 19 - viewChild函數的高階使用案例(二)- 將NgTemplate嵌入到ViewContainerRef中
下一篇
Day 21 - contentChild 函數介紹
系列文
Signal API in Angular39
圖片
  直播研討會
圖片
{{ item.channelVendor }} {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言